package net.pws.common.security.filter;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class SecurityFilterProxy implements Filter {
    
    private static final Log logger = LogFactory.getLog(SecurityFilterProxy.class);
    
    private FilterNode filterNode;
    
    public SecurityFilterProxy(FilterNode filterNode) {
        this.filterNode = filterNode;
    }
    
    public FilterNode getFilterNode() {
        return this.filterNode;
    }
    
    public void setFilterNode(FilterNode filterNode) {
        this.filterNode = filterNode;
    }
    
    public void init(FilterConfig filterConfig) throws ServletException {
        if (null == filterNode) {
            return;
        }
        
        filterNode.init(filterConfig);
    }
    
    public void doFilter(ServletRequest request,
                         ServletResponse response,
                         FilterChain chain) throws IOException,
                                           ServletException {
        FilterInvocation fi = new FilterInvocation(request, response, chain);
        
        if (filterNode == null) {
            if (logger.isDebugEnabled()) {
                logger.debug(fi.getRequestUrl() + " has an empty filter path");
            }
            
            chain.doFilter(request, response);
            
            return;
        }
        
        FilterRoute filterRoute = new FilterRoute(fi, filterNode);
        filterRoute.doFilter(fi.getRequest(), fi.getResponse());
    }
    
    public void destroy() {
        if (null == filterNode) {
            return;
        }
        
        filterNode.destroy();
        
    }
    
    public static FilterChainRoute getImplementation(FilterChain chain) {
        return (FilterChainRoute) chain;
    }
    
    class FilterRoute implements FilterChain, FilterChainRoute {
        
        private FilterInvocation fi;
        
        private boolean first = true;
        
        private FilterNode currentNode;
        
        public FilterRoute(FilterInvocation filterInvocation, FilterNode node) {
            this.fi = filterInvocation;
            this.currentNode = node;
        }
        
        public void doFilter(ServletRequest request, ServletResponse response) throws IOException,
                                                                              ServletException {
            if (currentNode == null) {
                if (logger.isDebugEnabled()) {
                    logger.debug(fi.getRequestUrl() + " reached end of additional filter chain; proceeding with original chain");
                }
                
                fi.getChain().doFilter(request, response);
                return;
            }
            
            if (first) {
                first = false;
                
                if (logger.isDebugEnabled()) {
                    logger.debug(fi.getRequestUrl() + " handled by "
                                 + currentNode.current().getClass().getName());
                }
                
                currentNode.current().doFilter(request, response, this);
                return;
            }
            
            if (!currentNode.hasNext()) {
                if (logger.isDebugEnabled()) {
                    logger.debug(fi.getRequestUrl() + " reached end of additional filter chain; proceeding with original chain");
                }
                
                fi.getChain().doFilter(request, response);
                return;
            }
            
            currentNode = currentNode.next();
            
            if (logger.isDebugEnabled()) {
                logger.debug(fi.getRequestUrl() + " at position "
                             + currentNode.current().getClass().getName());
            }
            
            currentNode.current().doFilter(request, response, this);
        }
        
        public void forceEnd(ServletRequest request, ServletResponse response) throws IOException,
                                                                              ServletException {
            currentNode = null;
            doFilter(request, response);
        }
        
        /**
         * Make sure this always be invoked within filter.doFilter
         */
        public void doFilter(int pathEntry,
                             ServletRequest request,
                             ServletResponse response) throws IOException,
                                                      ServletException {
            if (currentNode == null) {
                if (logger.isDebugEnabled()) {
                    logger.debug(fi.getRequestUrl() + " reached end of additional filter chain; proceeding with original chain");
                }
                
                fi.getChain().doFilter(request, response);
                return;
            }
            
            if (first) {
                if (logger.isDebugEnabled()) {
                    logger.debug(fi.getRequestUrl() + " at the first of filter chain ["
                                 + currentNode.current().getClass().getName()
                                 + "]; Choose branch is not suppported!");
                }
                
                throw new ServletException(fi.getRequestUrl() + " at the first of filter chain ["
                                           + currentNode.current()
                                                        .getClass()
                                                        .getName()
                                           + "]; Choose branch is not suppported!");
            }
            
            if (!currentNode.hasNext(pathEntry)) {
                if (logger.isDebugEnabled()) {
                    logger.debug(fi.getRequestUrl() + " couldn't forward to next by specified entry in the filter chain; Invalid path entry["
                                 + pathEntry
                                 + "] for "
                                 + currentNode.current().getClass().getName());
                }
                
                throw new ServletException("Invalid path entry[" + pathEntry
                                           + "] for "
                                           + currentNode.current()
                                                        .getClass()
                                                        .getName());
            }
            
            currentNode = currentNode.next(pathEntry);
            
            if (logger.isDebugEnabled()) {
                logger.debug(fi.getRequestUrl() + " at position "
                             + currentNode.current().getClass().getName());
            }
            
            currentNode.current().doFilter(request, response, this);
        }
    }
    
}
